import { Editor, posToDOMRect } from '@tiptap/core';
import { EditorState, Plugin, PluginKey } from '@tiptap/pm/state';
import { EditorView } from '@tiptap/pm/view';
import tippy, { Instance, Props } from 'tippy.js';

export interface ContentAIPluginProps {
  /**
   * 插件名称
   * @default 'contentAI'
   */
  pluginKey: PluginKey | string;

  /**
   * 编辑器实例
   * @default null
   */
  editor: Editor;

  /**
   * 包含菜单的 DOM 元素。
   * @default null
   */
  element: HTMLElement;

  /**
   * tippy.js 实例的选项。
   * @default {}
   * @see https://atomiks.github.io/tippyjs/v6/all-props/
   */
  tippyOptions?: Partial<Props>;

  /**
   * 决定是否显示菜单的函数。
   * 如果此函数返回“ false”，则菜单将被隐藏，否则将显示。
   * @default null
   */
  shouldShow?:
    | ((props: { editor: Editor; view: EditorView; state: EditorState; oldState?: EditorState; keyCode?: string }) => boolean)
    | null;

  getView?: (view: ContentAIView) => void;
  autoFocusSelector?: string;
}

export type ContentAIViewProps = ContentAIPluginProps & {
  /**
   * 编辑器视图
   */
  view: EditorView;
};

export class ContentAIView {
  editor: Editor;

  element: HTMLElement;

  view: EditorView;

  preventHide = false;

  tippy?: Instance;

  tippyOptions?: Partial<Props>;

  keyCode?: string;

  autoFocusSelector?: string;

  shouldShow: Exclude<ContentAIPluginProps['shouldShow'], null> = ({ state, keyCode }) => {
    if (keyCode !== 'Space') return false;
    const { selection } = state;
    const { $anchor, empty } = selection;
    const isRootDepth = $anchor.depth === 1;
    const isEmptyTextBlock = $anchor.parent.isTextblock && !$anchor.parent.type.spec.code && !$anchor.parent.textContent.replace(' ', '');
    if (!empty || !isRootDepth || !this.editor.isEditable || !isEmptyTextBlock) return false;
    return true;
  };

  constructor({ editor, element, view, tippyOptions = {}, shouldShow, getView, autoFocusSelector }: ContentAIViewProps) {
    this.editor = editor;
    this.element = element;
    this.view = view;
    this.autoFocusSelector = autoFocusSelector;

    if (shouldShow) this.shouldShow = shouldShow;
    if (getView) getView(this);

    this.element.addEventListener('mousedown', this.mousedownHandler, { capture: true });
    this.view.dom.addEventListener('keydown', this.keydownHandler, { capture: true });
    this.view.dom.addEventListener('scroll', this.scrollHandler);
    document.body.addEventListener('mousedown', this.bodyMousedownHandler);
    this.tippyOptions = tippyOptions;
    this.element.remove();
    this.element.style.visibility = 'visible';
  }

  scrollHandler = () => {
    const { state } = this.view;
    const { selection } = state;
    const { from, to } = selection;
    this.tippy?.setProps({
      getReferenceClientRect: this.tippyOptions?.getReferenceClientRect || (() => posToDOMRect(this.view, from, to)),
    });
  };

  onKeyDown = (e: KeyboardEvent) => {
    switch (e.code) {
      case 'Backspace': {
        const { value } = e.target as any;
        if (!value) {
          this.keyCode = undefined;
          this.hide();
          this.editor.commands.focus();
        }
        break;
      }
      case 'Enter': {
        break;
      }
    }
  };

  bodyMousedownHandler = () => {
    if (this.preventHide) {
      this.preventHide = false;
      return;
    }
    if (this.tippy?.state.isVisible) {
      this.keyCode = undefined;
      this.hide();
    }
  };

  keydownHandler = (e: KeyboardEvent) => {
    this.keyCode = e.code;
    const { editor, view } = this;
    if (e.code === 'Space' && this.shouldShow?.({ editor, view, state: view.state, keyCode: e.code })) {
      this.editor.commands.blur();
      if (this.autoFocusSelector) {
        setTimeout(() => {
          const node = this.element.querySelector(this.autoFocusSelector!) as HTMLElement;
          if (node) node.focus();
        });
      }
    }
  };

  mousedownHandler = () => {
    this.preventHide = true;
  };

  createTooltip() {
    const { element: editorElement } = this.editor.options;
    const editorIsAttached = !!editorElement.parentElement;
    if (this.tippy || !editorIsAttached) {
      return;
    }
    this.tippy = tippy(editorElement, {
      duration: 0,
      getReferenceClientRect: null,
      content: this.element,
      interactive: true,
      trigger: 'manual',
      placement: 'right',
      hideOnClick: 'toggle',
      showOnCreate: true,
      maxWidth: 'max-content',
      ...this.tippyOptions,
    });
  }

  update(view: EditorView, oldState?: EditorState) {
    const { state } = view;
    const { doc, selection } = state;
    const { from, to } = selection;
    const isSame = oldState && oldState.doc.eq(doc) && oldState.selection.eq(selection);
    if (isSame) return;

    this.createTooltip();

    const shouldShow = this.shouldShow?.({ editor: this.editor, view, state, oldState, keyCode: this.keyCode });

    if (!shouldShow) return this.hide();
    this.tippy?.setProps({
      getReferenceClientRect: this.tippyOptions?.getReferenceClientRect || (() => posToDOMRect(view, from, to)),
    });
    this.show();
  }

  show() {
    this.tippy?.show();
  }

  hide() {
    if (this.tippy?.state.isVisible) {
      this.tippy.hide();
      this.removeSpace();
    }
  }

  removeSpace() {
    const { state } = this.view;
    const { selection } = state;
    const { from, to } = selection;
    // 检查是否需要删除光标前的字符
    if (from === to && from > 0) {
      this.editor.commands.deleteRange({ from: from - 1, to: from });
      // 创建并应用一个删除光标前一个字符的事务
      // const transaction = state.tr.delete(from - 1, from);
      // this.view.dispatch(transaction);
    }
  }

  destroy() {
    this.tippy?.destroy();
    this.element.removeEventListener('mousedown', this.mousedownHandler, { capture: true });
    this.view.dom.removeEventListener('keydown', this.keydownHandler);
    this.view.dom.removeEventListener('scroll', this.scrollHandler);
    document.body.removeEventListener('mousedown', this.bodyMousedownHandler);
  }
}

export const ContentAIPlugin = (options: ContentAIPluginProps) => {
  return new Plugin({
    key: typeof options.pluginKey === 'string' ? new PluginKey(options.pluginKey) : options.pluginKey,
    view: (view) => new ContentAIView({ view, ...options }),
  });
};
